StatementHandler
前面我们说过,Executor
的主要职责是执行底层映射语句。
但是通过源码我们可以发现,Executor
执行的这些功能,都是通过StatementHandler
来完成的,Executor
只是负责缓存或者选择调用StatmentHandler
的具体的方法。
下面来看看StatementHandler
的具体实现
//新建StatmentHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//创建RoutingStatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//添加插件
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
这个方法是所有创建StatementHandler
的方法,可以看到都是统一的创建RoutingStatementHandler
.
RoutingStatementHandler
是一个代理类,里面会根据参数调用真正的StatmentHandler
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
为什么要这样做,而不是做一个简单工厂,返回真正的
StatementHandler
在RoutingStatementHandler
中,一共返回了3中StatementHandler
,分别是:
SimpleStatementHandler
:简单的Statement
,对应JDBC
中的SimpleStatement
PreparedStatementHandler
: 预编译的Statement
,对应JDBC
中的PreparedStatement
CallableStatementHandler
: 存储过程Statement
, 对应JDBC
中的CallableStatement
看到这里,可以发现,只要知道JDBC
中这些Statment
的区别,就能明白这些Handler
的区别,在MyBatis
中,默认使用的是PreparedStatmentHandler
,此Statement
会预编译SQL
,使得执行速度更快。
在MyBatis
中,使用StatmentHandler
分为3步
新建StatmentHandler
– -> prepare()
–> parameterize()
–> doAction()
BaseStatementHandler#prepare()
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
//设置错误日志
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
//交给各个子类创建statement
statement = instantiateStatement(connection);
//设置超时时间
setStatementTimeout(statement, transactionTimeout);
//设置一次获取的数据量
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
其中,
fetchSize
是用来设置数据库一次获取的数据量对于
MySQL
(其他数据库不一定,可能是10)默认情况下,比如select *
,数据库会直接返回所有的数据,而这个时候可能会使得JVM
OOM
,而此时则可以通过设置fetchSize
,使得Java
程序可以一点一点的处理。对于其他数据库,是支持
fetchSize
的时候,fetchSize
太小可能会影响性能,因此可以通过设置此值进行调试想要此值生效,必须开启事务
https://blog.csdn.net/seven_3306/article/details/9303979
PreparedStatementHandler#instantiateStatement()
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
//如果设置了需要获取自增长id
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
//设置JDBC需要返回id
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
//如果没有设置ResultSet 则使用默认的ResultSet
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
//否则使用用户设置色`ResultSet`
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
有关
ResultSetType
详细信息,可以见JDBC 查询的三大参数
ParameterSize
ParameterSize
大概的含义就是处理参数,主要在PreparedStatementHandler
中,用于setString() / setInteger()
等等
@Override
public void setParameters(PreparedStatement ps) {
//添加日志信息
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//获取此行命令需要的ParamMapping
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//遍历处理
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
//如果不是存储过程
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//获取属性名
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
}
//如果类型处理注册器中包含能够处理此类型的处理器
//则不用修改
else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
}
//否则尝试使用反射处理参数
else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//使用TypeHandler处理参数
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
//调用typeHandler处理参数
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
可以看到上面代码虽然比较多,但是其实就是循环处理需要的参数而已。
而对于参数分为两种:
- 能够直接被
typeHandler
处理的属性 - 能够通过反射获取其属性(Getter / Setter)
这里有一个小问题,在于上面的代码中,参数始终为parameterObject
那如果存在多个参数的情况怎么办?
参数处理
我们知道MyBatis
的接口是通过动态代理实现的。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
以上是动态代理接口的方法,可以看见参数其实是一个数组,也可以是一个可变参数,因次不管接口中包含多少个参数,
MyBatis
都会收到一个数组。当拿到数组后,
MyBatis
会结合注解进行处理:
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
}
//如果没有注解,并且参数数量为1,直接返回
else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
}
//否者使用HashMap进行包装
else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//添加key value
//例如@Param("name") 则 key 为 name , value 为实际的值
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
// 同时也会添加类似 key param1 value 为实际的值到map中
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
这里的代码便对应了MyBatis
的两种参数标记方法,第一种为@Param(name)
,第二种为param1
值得注意的是,paramSize()
只有在Statement
为PREPARED
才会有参数设置,否则MyBatis
将不会解析对应的参数。
剩下的操作便是执行真正的操作:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//获取Statement
PreparedStatement ps = (PreparedStatement) statement;
//执行
ps.execute();
//处理结果
return resultSetHandler.handleResultSets(ps);
}
这里看到执行便是通过JDBC
执行Statement
,然后调用ResultSetHandler
处理结果。
对于ResultSetHandler
在下一节介绍
总结
回忆前面几篇,我们可以分析出来,MyBatis
执行过程调用的组件:
首先,通过SqlSessionFactory
获取SqlSession
然后,在获取SqlSession
的过程中,会通过配置文件配置Transaction
然后,通过以上几个参数,构建出一个Executor
,Executor
分为3种,对于3中不同的处理/新建StatmentHandler
的行为
再然后,Executor
会调用StatementHandler
各个方法,配置参数,调用ParameterHandler
处理参数等,最后再进行执行,其中StatementHnalder
分为3中。STATEMENT
,PREPARED
,CALLBACK
,每个方法配置一个,默认为PREPARED
最后,StatementHandler
调用statement
执行SQL
,然后调用ResultSetHandler
处理结果